/*!
 * # Semantic UI - Sticky
 * http://github.com/semantic-org/semantic-ui/
 *
 *
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

(function($, window, document, undefined) {
  "use strict";

  window =
    typeof window != "undefined" && window.Math == Math
      ? window
      : typeof self != "undefined" && self.Math == Math
        ? self
        : Function("return this")();

  $.fn.sticky = function(parameters) {
    var $allModules = $(this),
      moduleSelector = $allModules.selector || "",
      time = new Date().getTime(),
      performance = [],
      query = arguments[0],
      methodInvoked = typeof query == "string",
      queryArguments = [].slice.call(arguments, 1),
      returnedValue;

    $allModules.each(function() {
      var settings = $.isPlainObject(parameters)
          ? $.extend(true, {}, $.fn.sticky.settings, parameters)
          : $.extend({}, $.fn.sticky.settings),
        className = settings.className,
        namespace = settings.namespace,
        error = settings.error,
        eventNamespace = "." + namespace,
        moduleNamespace = "module-" + namespace,
        $module = $(this),
        $window = $(window),
        $scroll = $(settings.scrollContext),
        $container,
        $context,
        selector = $module.selector || "",
        instance = $module.data(moduleNamespace),
        requestAnimationFrame =
          window.requestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.msRequestAnimationFrame ||
          function(callback) {
            setTimeout(callback, 0);
          },
        element = this,
        documentObserver,
        observer,
        module;

      module = {
        initialize: function() {
          module.determineContainer();
          module.determineContext();
          module.verbose("Initializing sticky", settings, $container);

          module.save.positions();
          module.checkErrors();
          module.bind.events();

          if (settings.observeChanges) {
            module.observeChanges();
          }
          module.instantiate();
        },

        instantiate: function() {
          module.verbose("Storing instance of module", module);
          instance = module;
          $module.data(moduleNamespace, module);
        },

        destroy: function() {
          module.verbose("Destroying previous instance");
          module.reset();
          if (documentObserver) {
            documentObserver.disconnect();
          }
          if (observer) {
            observer.disconnect();
          }
          $window
            .off("load" + eventNamespace, module.event.load)
            .off("resize" + eventNamespace, module.event.resize);
          $scroll.off(
            "scrollchange" + eventNamespace,
            module.event.scrollchange
          );
          $module.removeData(moduleNamespace);
        },

        observeChanges: function() {
          if ("MutationObserver" in window) {
            documentObserver = new MutationObserver(
              module.event.documentChanged
            );
            observer = new MutationObserver(module.event.changed);
            documentObserver.observe(document, {
              childList: true,
              subtree: true
            });
            observer.observe(element, {
              childList: true,
              subtree: true
            });
            observer.observe($context[0], {
              childList: true,
              subtree: true
            });
            module.debug("Setting up mutation observer", observer);
          }
        },

        determineContainer: function() {
          if (settings.container) {
            $container = $(settings.container);
          } else {
            $container = $module.offsetParent();
          }
        },

        determineContext: function() {
          if (settings.context) {
            $context = $(settings.context);
          } else {
            $context = $container;
          }
          if ($context.length === 0) {
            module.error(error.invalidContext, settings.context, $module);
            return;
          }
        },

        checkErrors: function() {
          if (module.is.hidden()) {
            module.error(error.visible, $module);
          }
          if (module.cache.element.height > module.cache.context.height) {
            module.reset();
            module.error(error.elementSize, $module);
            return;
          }
        },

        bind: {
          events: function() {
            $window
              .on("load" + eventNamespace, module.event.load)
              .on("resize" + eventNamespace, module.event.resize);
            // pub/sub pattern
            $scroll
              .off("scroll" + eventNamespace)
              .on("scroll" + eventNamespace, module.event.scroll)
              .on("scrollchange" + eventNamespace, module.event.scrollchange);
          }
        },

        event: {
          changed: function(mutations) {
            clearTimeout(module.timer);
            module.timer = setTimeout(function() {
              module.verbose(
                "DOM tree modified, updating sticky menu",
                mutations
              );
              module.refresh();
            }, 100);
          },
          documentChanged: function(mutations) {
            [].forEach.call(mutations, function(mutation) {
              if (mutation.removedNodes) {
                [].forEach.call(mutation.removedNodes, function(node) {
                  if (node == element || $(node).find(element).length > 0) {
                    module.debug(
                      "Element removed from DOM, tearing down events"
                    );
                    module.destroy();
                  }
                });
              }
            });
          },
          load: function() {
            module.verbose("Page contents finished loading");
            requestAnimationFrame(module.refresh);
          },
          resize: function() {
            module.verbose("Window resized");
            requestAnimationFrame(module.refresh);
          },
          scroll: function() {
            requestAnimationFrame(function() {
              $scroll.triggerHandler(
                "scrollchange" + eventNamespace,
                $scroll.scrollTop()
              );
            });
          },
          scrollchange: function(event, scrollPosition) {
            module.stick(scrollPosition);
            settings.onScroll.call(element);
          }
        },

        refresh: function(hardRefresh) {
          module.reset();
          if (!settings.context) {
            module.determineContext();
          }
          if (hardRefresh) {
            module.determineContainer();
          }
          module.save.positions();
          module.stick();
          settings.onReposition.call(element);
        },

        supports: {
          sticky: function() {
            var $element = $("<div/>"),
              element = $element[0];
            $element.addClass(className.supported);
            return $element.css("position").match("sticky");
          }
        },

        save: {
          lastScroll: function(scroll) {
            module.lastScroll = scroll;
          },
          elementScroll: function(scroll) {
            module.elementScroll = scroll;
          },
          positions: function() {
            var scrollContext = {
                height: $scroll.height()
              },
              element = {
                margin: {
                  top: parseInt($module.css("margin-top"), 10),
                  bottom: parseInt($module.css("margin-bottom"), 10)
                },
                offset: $module.offset(),
                width: $module.outerWidth(),
                height: $module.outerHeight()
              },
              context = {
                offset: $context.offset(),
                height: $context.outerHeight()
              },
              container = {
                height: $container.outerHeight()
              };
            if (!module.is.standardScroll()) {
              module.debug(
                "Non-standard scroll. Removing scroll offset from element offset"
              );

              scrollContext.top = $scroll.scrollTop();
              scrollContext.left = $scroll.scrollLeft();

              element.offset.top += scrollContext.top;
              context.offset.top += scrollContext.top;
              element.offset.left += scrollContext.left;
              context.offset.left += scrollContext.left;
            }
            module.cache = {
              fits: element.height + settings.offset <= scrollContext.height,
              sameHeight: element.height == context.height,
              scrollContext: {
                height: scrollContext.height
              },
              element: {
                margin: element.margin,
                top: element.offset.top - element.margin.top,
                left: element.offset.left,
                width: element.width,
                height: element.height,
                bottom: element.offset.top + element.height
              },
              context: {
                top: context.offset.top,
                height: context.height,
                bottom: context.offset.top + context.height
              }
            };
            module.set.containerSize();

            module.stick();
            module.debug("Caching element positions", module.cache);
          }
        },

        get: {
          direction: function(scroll) {
            var direction = "down";
            scroll = scroll || $scroll.scrollTop();
            if (module.lastScroll !== undefined) {
              if (module.lastScroll < scroll) {
                direction = "down";
              } else if (module.lastScroll > scroll) {
                direction = "up";
              }
            }
            return direction;
          },
          scrollChange: function(scroll) {
            scroll = scroll || $scroll.scrollTop();
            return module.lastScroll ? scroll - module.lastScroll : 0;
          },
          currentElementScroll: function() {
            if (module.elementScroll) {
              return module.elementScroll;
            }
            return module.is.top()
              ? Math.abs(parseInt($module.css("top"), 10)) || 0
              : Math.abs(parseInt($module.css("bottom"), 10)) || 0;
          },

          elementScroll: function(scroll) {
            scroll = scroll || $scroll.scrollTop();
            var element = module.cache.element,
              scrollContext = module.cache.scrollContext,
              delta = module.get.scrollChange(scroll),
              maxScroll =
                element.height - scrollContext.height + settings.offset,
              elementScroll = module.get.currentElementScroll(),
              possibleScroll = elementScroll + delta;
            if (module.cache.fits || possibleScroll < 0) {
              elementScroll = 0;
            } else if (possibleScroll > maxScroll) {
              elementScroll = maxScroll;
            } else {
              elementScroll = possibleScroll;
            }
            return elementScroll;
          }
        },

        remove: {
          lastScroll: function() {
            delete module.lastScroll;
          },
          elementScroll: function(scroll) {
            delete module.elementScroll;
          },
          minimumSize: function() {
            $container.css("min-height", "");
          },
          offset: function() {
            $module.css("margin-top", "");
          }
        },

        set: {
          offset: function() {
            module.verbose("Setting offset on element", settings.offset);
            $module.css("margin-top", settings.offset);
          },
          containerSize: function() {
            var tagName = $container.get(0).tagName;
            if (tagName === "HTML" || tagName == "body") {
              // this can trigger for too many reasons
              //module.error(error.container, tagName, $module);
              module.determineContainer();
            } else {
              if (
                Math.abs(
                  $container.outerHeight() - module.cache.context.height
                ) > settings.jitter
              ) {
                module.debug(
                  "Context has padding, specifying exact height for container",
                  module.cache.context.height
                );
                $container.css({
                  height: module.cache.context.height
                });
              }
            }
          },
          minimumSize: function() {
            var element = module.cache.element;
            $container.css("min-height", element.height);
          },
          scroll: function(scroll) {
            module.debug("Setting scroll on element", scroll);
            if (module.elementScroll == scroll) {
              return;
            }
            if (module.is.top()) {
              $module.css("bottom", "").css("top", -scroll);
            }
            if (module.is.bottom()) {
              $module.css("top", "").css("bottom", scroll);
            }
          },
          size: function() {
            if (
              module.cache.element.height !== 0 &&
              module.cache.element.width !== 0
            ) {
              element.style.setProperty(
                "width",
                module.cache.element.width + "px",
                "important"
              );
              element.style.setProperty(
                "height",
                module.cache.element.height + "px",
                "important"
              );
            }
          }
        },

        is: {
          standardScroll: function() {
            return $scroll[0] == window;
          },
          top: function() {
            return $module.hasClass(className.top);
          },
          bottom: function() {
            return $module.hasClass(className.bottom);
          },
          initialPosition: function() {
            return !module.is.fixed() && !module.is.bound();
          },
          hidden: function() {
            return !$module.is(":visible");
          },
          bound: function() {
            return $module.hasClass(className.bound);
          },
          fixed: function() {
            return $module.hasClass(className.fixed);
          }
        },

        stick: function(scroll) {
          var cachedPosition = scroll || $scroll.scrollTop(),
            cache = module.cache,
            fits = cache.fits,
            sameHeight = cache.sameHeight,
            element = cache.element,
            scrollContext = cache.scrollContext,
            context = cache.context,
            offset =
              module.is.bottom() && settings.pushing
                ? settings.bottomOffset
                : settings.offset,
            scroll = {
              top: cachedPosition + offset,
              bottom: cachedPosition + offset + scrollContext.height
            },
            direction = module.get.direction(scroll.top),
            elementScroll = fits ? 0 : module.get.elementScroll(scroll.top),
            // shorthand
            doesntFit = !fits,
            elementVisible = element.height !== 0;
          if (elementVisible && !sameHeight) {
            if (module.is.initialPosition()) {
              if (scroll.top >= context.bottom) {
                module.debug("Initial element position is bottom of container");
                module.bindBottom();
              } else if (scroll.top > element.top) {
                if (
                  element.height + scroll.top - elementScroll >=
                  context.bottom
                ) {
                  module.debug(
                    "Initial element position is bottom of container"
                  );
                  module.bindBottom();
                } else {
                  module.debug("Initial element position is fixed");
                  module.fixTop();
                }
              }
            } else if (module.is.fixed()) {
              // currently fixed top
              if (module.is.top()) {
                if (scroll.top <= element.top) {
                  module.debug("Fixed element reached top of container");
                  module.setInitialPosition();
                } else if (
                  element.height + scroll.top - elementScroll >=
                  context.bottom
                ) {
                  module.debug("Fixed element reached bottom of container");
                  module.bindBottom();
                }
                // scroll element if larger than screen
                else if (doesntFit) {
                  module.set.scroll(elementScroll);
                  module.save.lastScroll(scroll.top);
                  module.save.elementScroll(elementScroll);
                }
              }

              // currently fixed bottom
              else if (module.is.bottom()) {
                // top edge
                if (scroll.bottom - element.height <= element.top) {
                  module.debug(
                    "Bottom fixed rail has reached top of container"
                  );
                  module.setInitialPosition();
                }
                // bottom edge
                else if (scroll.bottom >= context.bottom) {
                  module.debug(
                    "Bottom fixed rail has reached bottom of container"
                  );
                  module.bindBottom();
                }
                // scroll element if larger than screen
                else if (doesntFit) {
                  module.set.scroll(elementScroll);
                  module.save.lastScroll(scroll.top);
                  module.save.elementScroll(elementScroll);
                }
              }
            } else if (module.is.bottom()) {
              if (scroll.top <= element.top) {
                module.debug(
                  "Jumped from bottom fixed to top fixed, most likely used home/end button"
                );
                module.setInitialPosition();
              } else {
                if (settings.pushing) {
                  if (module.is.bound() && scroll.bottom <= context.bottom) {
                    module.debug(
                      "Fixing bottom attached element to bottom of browser."
                    );
                    module.fixBottom();
                  }
                } else {
                  if (
                    module.is.bound() &&
                    scroll.top <= context.bottom - element.height
                  ) {
                    module.debug(
                      "Fixing bottom attached element to top of browser."
                    );
                    module.fixTop();
                  }
                }
              }
            }
          }
        },

        bindTop: function() {
          module.debug("Binding element to top of parent container");
          module.remove.offset();
          $module
            .css({
              left: "",
              top: "",
              marginBottom: ""
            })
            .removeClass(className.fixed)
            .removeClass(className.bottom)
            .addClass(className.bound)
            .addClass(className.top);
          settings.onTop.call(element);
          settings.onUnstick.call(element);
        },
        bindBottom: function() {
          module.debug("Binding element to bottom of parent container");
          module.remove.offset();
          $module
            .css({
              left: "",
              top: ""
            })
            .removeClass(className.fixed)
            .removeClass(className.top)
            .addClass(className.bound)
            .addClass(className.bottom);
          settings.onBottom.call(element);
          settings.onUnstick.call(element);
        },

        setInitialPosition: function() {
          module.debug("Returning to initial position");
          module.unfix();
          module.unbind();
        },

        fixTop: function() {
          module.debug("Fixing element to top of page");
          if (settings.setSize) {
            module.set.size();
          }
          module.set.minimumSize();
          module.set.offset();
          $module
            .css({
              left: module.cache.element.left,
              bottom: "",
              marginBottom: ""
            })
            .removeClass(className.bound)
            .removeClass(className.bottom)
            .addClass(className.fixed)
            .addClass(className.top);
          settings.onStick.call(element);
        },

        fixBottom: function() {
          module.debug("Sticking element to bottom of page");
          if (settings.setSize) {
            module.set.size();
          }
          module.set.minimumSize();
          module.set.offset();
          $module
            .css({
              left: module.cache.element.left,
              bottom: "",
              marginBottom: ""
            })
            .removeClass(className.bound)
            .removeClass(className.top)
            .addClass(className.fixed)
            .addClass(className.bottom);
          settings.onStick.call(element);
        },

        unbind: function() {
          if (module.is.bound()) {
            module.debug("Removing container bound position on element");
            module.remove.offset();
            $module
              .removeClass(className.bound)
              .removeClass(className.top)
              .removeClass(className.bottom);
          }
        },

        unfix: function() {
          if (module.is.fixed()) {
            module.debug("Removing fixed position on element");
            module.remove.minimumSize();
            module.remove.offset();
            $module
              .removeClass(className.fixed)
              .removeClass(className.top)
              .removeClass(className.bottom);
            settings.onUnstick.call(element);
          }
        },

        reset: function() {
          module.debug("Resetting elements position");
          module.unbind();
          module.unfix();
          module.resetCSS();
          module.remove.offset();
          module.remove.lastScroll();
        },

        resetCSS: function() {
          $module.css({
            width: "",
            height: ""
          });
          $container.css({
            height: ""
          });
        },

        setting: function(name, value) {
          if ($.isPlainObject(name)) {
            $.extend(true, settings, name);
          } else if (value !== undefined) {
            settings[name] = value;
          } else {
            return settings[name];
          }
        },
        internal: function(name, value) {
          if ($.isPlainObject(name)) {
            $.extend(true, module, name);
          } else if (value !== undefined) {
            module[name] = value;
          } else {
            return module[name];
          }
        },
        debug: function() {
          if (!settings.silent && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            } else {
              module.debug = Function.prototype.bind.call(
                console.info,
                console,
                settings.name + ":"
              );
              module.debug.apply(console, arguments);
            }
          }
        },
        verbose: function() {
          if (!settings.silent && settings.verbose && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            } else {
              module.verbose = Function.prototype.bind.call(
                console.info,
                console,
                settings.name + ":"
              );
              module.verbose.apply(console, arguments);
            }
          }
        },
        error: function() {
          if (!settings.silent) {
            module.error = Function.prototype.bind.call(
              console.error,
              console,
              settings.name + ":"
            );
            module.error.apply(console, arguments);
          }
        },
        performance: {
          log: function(message) {
            var currentTime, executionTime, previousTime;
            if (settings.performance) {
              currentTime = new Date().getTime();
              previousTime = time || currentTime;
              executionTime = currentTime - previousTime;
              time = currentTime;
              performance.push({
                Name: message[0],
                Arguments: [].slice.call(message, 1) || "",
                Element: element,
                "Execution Time": executionTime
              });
            }
            clearTimeout(module.performance.timer);
            module.performance.timer = setTimeout(
              module.performance.display,
              0
            );
          },
          display: function() {
            var title = settings.name + ":",
              totalTime = 0;
            time = false;
            clearTimeout(module.performance.timer);
            $.each(performance, function(index, data) {
              totalTime += data["Execution Time"];
            });
            title += " " + totalTime + "ms";
            if (moduleSelector) {
              title += " '" + moduleSelector + "'";
            }
            if (
              (console.group !== undefined || console.table !== undefined) &&
              performance.length > 0
            ) {
              console.groupCollapsed(title);
              if (console.table) {
                console.table(performance);
              } else {
                $.each(performance, function(index, data) {
                  console.log(
                    data["Name"] + ": " + data["Execution Time"] + "ms"
                  );
                });
              }
              console.groupEnd();
            }
            performance = [];
          }
        },
        invoke: function(query, passedArguments, context) {
          var object = instance,
            maxDepth,
            found,
            response;
          passedArguments = passedArguments || queryArguments;
          context = element || context;
          if (typeof query == "string" && object !== undefined) {
            query = query.split(/[\. ]/);
            maxDepth = query.length - 1;
            $.each(query, function(depth, value) {
              var camelCaseValue =
                depth != maxDepth
                  ? value +
                    query[depth + 1].charAt(0).toUpperCase() +
                    query[depth + 1].slice(1)
                  : query;
              if (
                $.isPlainObject(object[camelCaseValue]) &&
                depth != maxDepth
              ) {
                object = object[camelCaseValue];
              } else if (object[camelCaseValue] !== undefined) {
                found = object[camelCaseValue];
                return false;
              } else if ($.isPlainObject(object[value]) && depth != maxDepth) {
                object = object[value];
              } else if (object[value] !== undefined) {
                found = object[value];
                return false;
              } else {
                return false;
              }
            });
          }
          if ($.isFunction(found)) {
            response = found.apply(context, passedArguments);
          } else if (found !== undefined) {
            response = found;
          }
          if ($.isArray(returnedValue)) {
            returnedValue.push(response);
          } else if (returnedValue !== undefined) {
            returnedValue = [returnedValue, response];
          } else if (response !== undefined) {
            returnedValue = response;
          }
          return found;
        }
      };

      if (methodInvoked) {
        if (instance === undefined) {
          module.initialize();
        }
        module.invoke(query);
      } else {
        if (instance !== undefined) {
          instance.invoke("destroy");
        }
        module.initialize();
      }
    });

    return returnedValue !== undefined ? returnedValue : this;
  };

  $.fn.sticky.settings = {
    name: "Sticky",
    namespace: "sticky",

    silent: false,
    debug: false,
    verbose: true,
    performance: true,

    // whether to stick in the opposite direction on scroll up
    pushing: false,

    context: false,
    container: false,

    // Context to watch scroll events
    scrollContext: window,

    // Offset to adjust scroll
    offset: 0,

    // Offset to adjust scroll when attached to bottom of screen
    bottomOffset: 0,

    // will only set container height if difference between context and container is larger than this number
    jitter: 5,

    // set width of sticky element when it is fixed to page (used to make sure 100% width is maintained if no fixed size set)
    setSize: true,

    // Whether to automatically observe changes with Mutation Observers
    observeChanges: false,

    // Called when position is recalculated
    onReposition: function() {},

    // Called on each scroll
    onScroll: function() {},

    // Called when element is stuck to viewport
    onStick: function() {},

    // Called when element is unstuck from viewport
    onUnstick: function() {},

    // Called when element reaches top of context
    onTop: function() {},

    // Called when element reaches bottom of context
    onBottom: function() {},

    error: {
      container: "Sticky element must be inside a relative container",
      visible:
        "Element is hidden, you must call refresh after element becomes visible. Use silent setting to surpress this warning in production.",
      method: "The method you called is not defined.",
      invalidContext: "Context specified does not exist",
      elementSize:
        "Sticky element is larger than its container, cannot create sticky."
    },

    className: {
      bound: "bound",
      fixed: "fixed",
      supported: "native",
      top: "top",
      bottom: "bottom"
    }
  };
})(jQuery, window, document);
